/**
 * 
 */
package com.orcwar.engine;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import android.util.Log;

import com.orcwar.engine.level.DefaultLevelGenerator;
import com.orcwar.engine.level.OWLevelBuilder;
import com.orcwar.engine.level.OWLevelBuilder.OWLevelParameters;
import com.orcwar.engine.pathfinding.AStarPathFinder;
import com.orcwar.engine.pathfinding.TileScanner;
import com.orcwar.engine.tile.OWAttackTile;
import com.orcwar.engine.units.OWBouclieur;
import com.orcwar.engine.units.OWFleauteur;
import com.orcwar.engine.units.OWHacheur;
import com.orcwar.engine.units.OWHommeLance;
import com.orcwar.engine.units.OWTireurDeLoin;
import com.orcwar.enums.DirectionEnum;
import com.orcwar.thread.MainThread;

/**
 * @author Callil
 * 
 */
public class OWGame {

	/** Logger tag. */
	private static final String TAG = OWGame.class.getSimpleName();

	/** The lone instance. */
	private static OWGame instance;

	// TODO move elsewhere (activity?) along with start/(un)pause/stop/leaveGame
	/** The thread corresponding to the game loop. */
	private MainThread thread;

	/** The game map. */
	private OWMap map;

	/** The list of players. Probably always with 2 elements. */
	private List<OWPlayer> players;
	/** Index of the current player in the {code players} list. */
	private int currentPlayer;
	
	 //TODO remove (find a solution to differenciate them)
	public OWPlayer userPlayer;
	public OWPlayer aiPlayer;

	private int level;

	/** The current turn for the current level. */
	private int currentTurn;

	private OWLevelParameters levelParameters;
	
	/** Game listeners that want to receive notifications each time the game
	 * state is changed. */
	private Set<IOWGameListener> gameListeners;

	private DefaultLevelGenerator levelGenerator;

	private AStarPathFinder pathfinder;

	/**
	 * Get the singleton.
	 * 
	 * @return the single instance of OWGame
	 */
	public static synchronized OWGame getInstance() {
		if (instance == null) {
			Log.e("OrcWar", "FUCK YOU - INITIALIZE THE GAME FIRST");
			throw new IllegalStateException(
					"Initialize OWGame with a view before calling it with getInstance.");
		}
		return instance;
	}

	/**
	 * Get the singleton.
	 * 
	 * @param view
	 * 
	 * @return the single instance of OWGame
	 */
	public static synchronized OWGame getInstance(MainThread thread) {
		if (instance == null) {
			instance = new OWGame(thread);
		}
		return instance;
	}

	/**
	 * Private constructor.
	 */
	private OWGame(MainThread thread) {
		// create the game loop thread
		this.thread = thread;
		
		// Create a new game!
		this.levelGenerator = new DefaultLevelGenerator();
		level = 0;
		map = new OWMap();
		pathfinder = new AStarPathFinder(map, false);
		gameListeners = new HashSet<IOWGameListener>();
		
		players = new ArrayList<OWPlayer>();
		currentPlayer = -1;
		
		initLevel(level);
		currentTurn = 1;
		startTurn();
	}
	
	/**
	 * Start the game.
	 */
	public void start() {
		// Notify listeners
		for(IOWGameListener listener : gameListeners)
		{
			listener.onLevelStart(map);
		}
		
		thread.setRunning(true);
		thread.start();
	}

	/**
	 * Initialize the level.
	 */
	private void initLevel(int level) {
		levelParameters = OWLevelBuilder.getLevelParameters(level);
		buildLevel(levelParameters);
	}
	

	public void buildLevel(OWLevelParameters owLevelParameters) {

		// create players
		OWPlayer player = new OWPlayer();
		OWPlayer enemy = new OWPlayer();
		addPlayers(player);
		addPlayers(enemy);
		userPlayer = player;
		aiPlayer = enemy;
		
		// build all units
		List<OWUnit> enemyUnits = levelGenerator.buildEnemies(owLevelParameters,
				enemy);
		for (OWUnit enemyUnit : enemyUnits) {
			enemyUnit.setOrientation(DirectionEnum.DOWN);
		}
		Log.d(OWMap.class.getName(), enemyUnits.toString());

		OWUnit unit1 = new OWFleauteur(1, player);
		OWUnit unit2 = new OWHacheur(2, player);
		OWUnit unit3 = new OWHommeLance(3, player);
		OWUnit unit4 = new OWTireurDeLoin(4, player);
		OWUnit unit5 = new OWBouclieur(5, player);

		List<OWUnit> units = new ArrayList<OWUnit>();
		units.add(unit1);
		units.add(unit2);
		units.add(unit3);
		units.add(unit4);
		units.add(unit5);

		// build map
		List<List<OWTile>> builtTiles = levelGenerator.buildTiles(
				owLevelParameters, enemyUnits.size(), units.size());
		while (builtTiles == null) {
			builtTiles = levelGenerator.buildTiles(owLevelParameters,
					enemyUnits.size(), units.size());
		}
		map.setTiles(builtTiles);

		levelGenerator.placeUnits(map, enemyUnits, units, owLevelParameters);

		player.setUnits(units);
		enemy.setUnits(enemyUnits);
		
		//Notify listeners
		for(IOWGameListener listener : gameListeners)
		{
			listener.onLevelStart(map);
		}
	}

	
	// ******************//
	// UNIT INTERACTIONS //

	/**
	 * Make the unit follow a path.
	 * 
	 * @param unit
	 *            The unit
	 * @param path
	 *            The list of tiles to cross to arrive to the destination. IT
	 *            SHOULDN'T CONTAIN the unit's source tile.
	 */
	public void move(OWUnit unit, List<OWTile> path) {
		OWTile startingTile = unit.getTile();
		
		OWTile currentDestination = null;
		OWTile currentPosition = startingTile;
		int i = 0;

		while (i < path.size()) {
			currentDestination = path.get(i);

			if (unit.onMove(currentPosition, currentDestination)) {
				// Moving from the fromTile to the currentDestination is
				// accepted
				unit.useMovePoint();
				currentPosition.setCurrentUnit(null);
				currentDestination.setCurrentUnit(unit, false);
			} else {
				// Movement impossible, stop here
				break;
			}

			i++;
			currentPosition = currentDestination;
		}

		// Sub list to keep only the tiles where the unit can actually go
		path.subList(0, i);
		
		//Restore the starting tile at the beginning of the list
		path.add(0, startingTile);

		//Notify listeners
		for(IOWGameListener listener : gameListeners)
		{
			listener.onUnitMove(unit, startingTile, currentPosition, path);
		}
		
		checkStateAfterUnitAction(unit);
	}


	/**
	 * Attack a TILE (which does not necessarily contain a unit).
	 * 
	 * @param unit
	 *            the unit attacking
	 * @param target
	 *            the tile attacked
	 */
	public void attack(OWUnit unit, OWAttackTile target) {
		Log.d(TAG, "Unit " + unit + " attacks the tile with ID = "
						+ target.getId());
		List<OWUnit> targetedUnits = new ArrayList<OWUnit>();
		boolean success = false;
		
		unit.useAttackPoint();
		
		//Attack the target
		success = performAttack(unit, target, targetedUnits);
		
		//If the target had linked tiles, attack them too !
		if (target.getLinkedTiles() != null) {
			for (OWAttackTile attackTile : target.getLinkedTiles()) {
				if (performAttack(unit, attackTile, targetedUnits)) success = true;
			}
		}
		
		// Notify listeners
		for(IOWGameListener listener : gameListeners)
		{
			listener.onUnitAttack(unit, target, success, targetedUnits);
		}
	}

	/**
	 * @param unit
	 * @param target
	 */
	private boolean performAttack(OWUnit unit, OWAttackTile target, List<OWUnit> targetedUnits) {
		boolean success = false;
		if (unit.onAttack(target.getId())) {
			success = true;
		}
		
		if (success && target.getCurrentUnit() != null) {
			targetedUnits.add(target.getCurrentUnit());
		}
		
		return success;
	}
	
	
	/**
	 * Finish an attack (after the animation) : kill the unit, remove it
	 * from the map and from the player and call onUnitDeath.
	 * @param targetedUnit the dead unit
	 */
	public void finishAttack(OWUnit unit, boolean success, List<OWUnit> targetedUnits) {
		// ATTACK UNITS
		for (OWUnit targetedUnit : targetedUnits) {
			targetedUnit.onDie();
			map.removeUnit(targetedUnit);
			targetedUnit.getPlayer().getUnits().remove(targetedUnit);
				
			// Notify listeners
			for(IOWGameListener listener : gameListeners)
			{
				listener.onUnitDeath(targetedUnit);
			}
		}
		
		checkStateAfterUnitAction(unit);
	}
	

	/**
	 * End the player's turn.
	 */
	public void pass(OWPlayer player) {
		if(! getCurrentPlayer().equals(player))
		{
			throw new IllegalStateException("FUCK YOU! This is not your turn "
					+ player);
		}
		
		for(OWUnit unit : player.getUnits())
		{
			endUnitTurn(unit);
		}
		endTurn();
	}
	
	private void checkStateAfterUnitAction(OWUnit unit)
	{
		// Check level end
		checkLevelEnd();
		
		// Check unit turn end
		if (unit.getAttackPoints() == 0 && unit.getMovePoints() == 0) {
			endUnitTurn(unit);
			
			// Check player turn end
			OWPlayer currentPlayer = getCurrentPlayer();
			boolean turnEnded = true;
			for(OWUnit playerUnit : currentPlayer.getUnits())
			{
				if(! playerUnit.isTurnEnded())
				{
					turnEnded = false;
					break;
				}
			}
			if(turnEnded)
			{
				endTurn();
			}
		}
	}

	private void checkLevelEnd() {
		
		// Count players with at least one unit left
		int alivePlayerCount = 0;
		OWPlayer lastPlayer = null;
		for(OWPlayer player : players)
		{
			if(! player.getUnits().isEmpty())
			{
				alivePlayerCount++;
				lastPlayer = player;
			}
		}
		
		if(alivePlayerCount <= 1)
		{
			endLevel(lastPlayer);
		}
	}

	/**
	 * End a unit's turn.
	 * 
	 * @param unit
	 *            the unit
	 */
	private void endUnitTurn(OWUnit unit) {
		// The unit's turn has ended
		unit.setTurnEnded(true);
		
		// Notify listeners
		for(IOWGameListener listener : gameListeners)
		{
			listener.onUnitTurnEnd(unit);
		}
	}

	/**
	 * Start a new level with higher difficulty.
	 */
	private void endLevel(OWPlayer winner) {
		// Notify listeners
		for(IOWGameListener listener : gameListeners)
		{
			listener.onLevelEnd(map, winner);
		}
		
		level++;
		initLevel(level);
	}

	private void startTurn() {
		currentPlayer++;
		if(currentPlayer >= players.size())
		{
			currentPlayer = 0;
			currentTurn++;
			map.startNewTurnForAllUnits();
		}
		
		if(getCurrentPlayer().getUnits().size() == 0)
		{
			// Skip this player's turn if all units are dead
			startTurn();
		}
		else
		{		
			// Notify listeners and run callback if applicable
			Runnable callback = null;
			for(IOWGameListener listener : gameListeners)
			{
				Runnable ret = listener.onTurnStart(getCurrentPlayer(), currentTurn);
				if(ret != null)
				{
					if(callback == null)
					{
						callback = ret;
					}
					else
					{
						Log.e(TAG, "FUCK YOU! Only one callback is allowed per turn");
					}
				}
			}
			if(callback != null)
			{
				callback.run();
			}
		}
	}
	
	private void endTurn()
	{
		OWPlayer currentPlayer = getCurrentPlayer();
		Log.d(TAG, "End of turn " + currentTurn + " for player " + currentPlayer);
		
		// Notify listeners
		for(IOWGameListener listener : gameListeners)
		{
			listener.onTurnEnd(currentPlayer);
		}
		
		// Start next turn
		startTurn();
	}

	// UNIT INTERACTIONS //
	// *******************//

	// ********************//
	// GAME CYCLE OF LIFE //

	/**
	 * Pause the game (ie : the thread is paused and no more update() nor draw()
	 * are called).
	 */
	public void pause() {
		thread.setPause(true);
	}

	/** Unpause the game, ie relaunch th thread. */
	public void unpause() {
		thread.setPause(false);
	}

	/** Pause the game and prepare the restoration. */
	public void stop() {
		thread.setRestoring();
	}

	/**
	 * QUit the game
	 */
	public void leaveGame() {
		// tell the thread to shut down and wait for it to finish
		// this is a clean shutdown
		thread.setRunning(false);
		boolean retry = true;
		while (retry) {
			try {
				thread.join();
				retry = false;
			} catch (InterruptedException e) {
				// try again shutting down the thread
			}
		}
		Log.d(this.getClass().getName(), "Thread was shut down cleanly");
	}

	// END OF GAME CYCLE OF LIFE //
	// ***************************//

	// *********************//
	// GETTERS AND SETTERS //
	// *********************//

	/**
	 * @return the map
	 */
	public OWMap getMap() {
		return map;
	}

	/**
	 * @param map
	 *            the map to set
	 */
	public void setMap(OWMap map) {
		this.map = map;
	}

	/**
	 * @return the players
	 */
	public List<OWPlayer> getPlayers() {
		return Collections.unmodifiableList(players);
	}

	/**
	 * @param player
	 *            the player to add
	 */
	public void addPlayers(OWPlayer player) {
		this.players.add(player);
	}
	
	public OWPlayer getCurrentPlayer()
	{
		try
		{
			return this.players.get(currentPlayer);
		}
		catch(IndexOutOfBoundsException ioobe)
		{
			Log.e(TAG, "FUCK YOU! Add players first", ioobe);
			return null;
		}
	}

	public OWLevelParameters getCurrentLevelInfo() {
		return levelParameters;
	}

	/**
	 * @return the thread
	 */
	public MainThread getThread() {
		return thread;
	}

	/**
	 * @param thread
	 *            the thread to set
	 */
	public void setThread(MainThread thread) {
		this.thread = thread;
	}

	/**
	 * @return the currentTurn
	 */
	public int getCurrentTurn() {
		return currentTurn;
	}

	/**
	 * @param currentTurn
	 *            the currentTurn to set
	 */
	public void setCurrentTurn(int currentTurn) {
		this.currentTurn = currentTurn;
	}

	public Set<OWTile> findPossibleTilesToMove(OWUnit unit) {
		return TileScanner.findPossibleTilesToMove(map, unit, pathfinder);
	}

	/**
	 * Find where the unit can attack.
	 * @param unit the unit
	 * @param possibleMoves the tiles where the unit can go. If null, just
	 * find where the unit can attack from it's current position.
	 * @return A set of tiles where the unit can attack
	 */
	public Set<OWAttackTile> findPossibleTilesToAttack(OWUnit unit, Set<OWTile> possibleMoves) {
		if (possibleMoves != null)
			return TileScanner.findPossibleTilesToAttack(map, unit, possibleMoves);
		else
			return TileScanner.findPossibleTilesToAttack(map, unit);
	}

	public Map<OWTile, DirectionEnum> findPath(OWUnit unit, OWTile unitTile,
			OWTile destinationTile) {
		List<OWTile> path = pathfinder
				.findPath(unit, unitTile.getX(), unitTile.getY(),
						destinationTile.getX(), destinationTile.getY());
		Map<OWTile, DirectionEnum> directionnedPath = new LinkedHashMap<OWTile, DirectionEnum>();
		for (int i = 0; i < path.size(); i++) {
			OWTile tile = path.get(i);
			if (i == 0) {
				directionnedPath.put(tile, unitTile.directionToTile(tile));
			} else {
				OWTile previousTile = path.get(i - 1);
				directionnedPath.put(tile, previousTile.directionToTile(tile));
			}
		}
		return directionnedPath;
	}

	/**
	 * @return the gameListeners
	 */
	public Set<IOWGameListener> getGameListeners() {
		return Collections.unmodifiableSet(gameListeners);
	}

	/**
	 * @param gameListener the gameListener to add
	 */
	public void addGameListener(IOWGameListener gameListener) {
		gameListeners.add(gameListener);
	}
	
	/**
	 * @param gameListener the gameListener to remove
	 */
	public boolean removeGameListener(IOWGameListener gameListener) {
		return gameListeners.remove(gameListener);
	}
}
